﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using System.Dynamic;
using System.Linq;
using System.Reflection;
using Dapper;
using Hx.Infrastructure.Common;

namespace Hx.Domain.Repositories.Dapper
{
    public static class DapperExtension
    {
        private static readonly ConcurrentDictionary<Type, List<PropertyInfo>> ParamCache = new ConcurrentDictionary<Type, List<PropertyInfo>>();

        public static void Insert<T>(this IDbConnection connection, T data, IDbTransaction transaction = null, int? commandTimeout = null)
        {
            var obj = data as object;
            var tableName = typeof(T).Name;
            var properties = GetProperties(obj);
            var columns = string.Join(",", properties);
            var values = string.Join(",", properties.Select(p => "@" + p));
            var sql = string.Format("INSERT INTO Hx_{0} ({1}) VALUES ({2})", tableName, columns, values);

            connection.Execute(sql, obj, transaction, commandTimeout);
        }

        public static int Update<T>(this IDbConnection connection, T data, dynamic condition, IDbTransaction transaction = null, int? commandTimeout = null)
        {
            var obj = data as object;
            var conditionObj = condition as object;
            var tableName = typeof(T).Name;
            var updatePropertyInfos = GetPropertyInfos(obj);
            var wherePropertyInfos = GetPropertyInfos(conditionObj);

            var updateProperties = updatePropertyInfos.Select(p => p.Name);
            var whereProperties = wherePropertyInfos.Select(p => p.Name).ToList();

            var updateFields = String.Join(",", updateProperties.Select(p => p + " = @" + p));
            var whereFields = String.Empty;

            if (whereProperties.Any())
                whereFields = " WHERE " + string.Join(" AND ", whereProperties.Select(p => p + " = @HxP_" + p));

            var sql = string.Format("UPDATE Hx_{0} SET {1}{2}", tableName, updateFields, whereFields);

            var parameters = new DynamicParameters(data);
            var expandoObject = new ExpandoObject() as IDictionary<string, object>;
            wherePropertyInfos.ForEach(p => expandoObject.Add("HxP_" + p.Name, p.GetValue(conditionObj, null)));
            parameters.AddDynamicParams(expandoObject);

            return connection.Execute(sql, parameters, transaction, commandTimeout);
        }

        public static int Delete<T>(this IDbConnection connection, dynamic condition, IDbTransaction transaction = null, int? commandTimeout = null)
        {
            var conditionObj = condition as object;
            var whereFields = string.Empty;
            var tableName = typeof(T).Name;
            var whereProperties = GetProperties(conditionObj);
            if (whereProperties.Count > 0)
                whereFields = " WHERE " + string.Join(" AND ", whereProperties.Select(p => p + " = @" + p));

            var sql = string.Format("DELETE FROM Hx_{0}{1}", tableName, whereFields);

            return connection.Execute(sql, conditionObj, transaction, commandTimeout);
        }

        public static T Get<T>(this IDbConnection connection, object condition, string columns = "*", bool isOr = false, IDbTransaction transaction = null, int? commandTimeout = null)
            where T : class,IAggregateRoot
        {
            var tableName = typeof(T).Name;
            return connection.Query<T>(BuildQuerySql(condition, tableName, columns, isOr), condition, transaction, true, commandTimeout).FirstOrDefault();
        }

        public static int GetCount(this IDbConnection connection, object condition, bool isOr = false, IDbTransaction transaction = null, int? commandTimeout = null)
        {
            return QueryList<int>(connection, condition, "COUNT(1)", isOr, transaction, commandTimeout).Single();
        }

        public static IEnumerable<T> QueryList<T>(this IDbConnection connection, object condition, string columns = "*", bool isOr = false, IDbTransaction transaction = null, int? commandTimeout = null)
        {
            var tableName = typeof(T).Name;
            return connection.Query<T>(BuildQuerySql(condition, tableName, columns, isOr), condition, transaction, true, commandTimeout);
        }

        public static PagedResult<T> QueryPaged<T>(this IDbConnection connection, dynamic condition, string orderBy, int pageIndex, int pageSize, string columns = "*", bool isOr = false, IDbTransaction transaction = null, int? commandTimeout = null)
        {
            var conditionObj = condition as object;
            var whereFields = string.Empty;
            var tableName = typeof(T).Name;
            var properties = GetProperties(conditionObj);
            if (properties.Count > 0)
            {
                var separator = isOr ? " OR " : " AND ";
                whereFields = " WHERE " + string.Join(separator, properties.Select(p => p + " = @" + p));
            }
            var sql = string.Format("SELECT {0} FROM Hx_{2}{3} ORDER BY {1} LIMIT {4} AND {5}", columns, orderBy, tableName, whereFields, (pageIndex - 1) * pageSize + 1, pageIndex * pageSize);

            var result = connection.Query<T>(sql, conditionObj, transaction, true, commandTimeout).ToList();

            var recordCount = GetCount(connection, condition, isOr, transaction, commandTimeout);

            return new PagedResult<T>(recordCount, ((recordCount - 1) / pageSize + 1), pageSize, pageIndex, result);
        }

        private static string BuildQuerySql(dynamic condition, string tableName, string selectPart = "*", bool isOr = false)
        {
            var conditionObj = condition as object;
            var properties = GetProperties(conditionObj);
            if (properties.Count == 0)
                return string.Format("SELECT {1} FROM Hx_{0}", tableName, selectPart);

            var separator = isOr ? " OR " : " AND ";
            var wherePart = string.Join(separator, properties.Select(p => p + " = @" + p));

            return string.Format("SELECT {2} FROM Hx_{0} WHERE {1}", tableName, wherePart, selectPart);
        }

        private static List<string> GetProperties(object obj)
        {
            if (obj == null)
            {
                return new List<string>();
            }

            var parameters = obj as DynamicParameters;
            if (parameters != null)
            {
                return parameters.ParameterNames.ToList();
            }
            return GetPropertyInfos(obj).Select(x => x.Name).ToList();
        }

        private static List<PropertyInfo> GetPropertyInfos(object obj)
        {
            if (obj == null)
            {
                return new List<PropertyInfo>();
            }

            List<PropertyInfo> properties;
            var objType = obj.GetType();
            if (ParamCache.TryGetValue(objType, out properties))
            {
                return properties.ToList();
            }

            properties = objType.GetProperties(BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.Public).ToList();
            ParamCache[objType] = properties;
            return properties;
        }
    }
}